iOS中使用autolayout来进行UITableView的布局(2)

由于昨天翻译的一篇内容是原作者在2014年写的,可能有点老。所以这次翻译的是stackoverflow中smileyborg解释的获得1.8k的回答:原文链接


###使用AutoLayout来动态显示UITableView中Cell的高度

TL;DR:(Too long,Don’t read,太长不看)如果你不想阅读内容,你可以直接跳转到这个github上简单的工程:

####概念描述

最开始的两步与你所开发的iOS的版本号无关

#####1.设置并添加约束
在你的UITableViewCell的子类中,通过约束你的cell的subView从而使得他们有自己的边框,从而撑起这个cell的contentView(最主要的是上下边的约束)。

注意:不要设置cell的subView与cell本身的约束,只能和cell的contentView约束。

让我们使用内容真正的大小来设置tableViewCell的内容,并通过设置内容在垂直方向上的压缩阻力(content compression resistance)内容收缩约束(content hugging constraints)来重新加载他的尺寸。(如果对于content Compression Resistance和Content Hugging Constraints有疑惑可以点击:这个连接

记住,这个方法(指设置压缩阻力和内容收缩约束)是为了能够cell的subView能够在垂直方向上能够与cell相连接,从而当内容“产生压力”的时候他们能够伸展内容的View从而适应他们。就如下面的这个例子中展出的部分约束(不是全部!):exampleImage

你能够想象,就如例子中一样,当内容在多行的label中不断增加的时候,label将会更好的变宽从而使得使得内容变得更加合适。(当然,为了能够使Cell更好的显示出来,你需要设置正确的约束)

设置正确的约束是使用AutoLayout动态获得cell高度最困难同时也是最重要的一步。没有这一步,其余的都只是无用功。所以在这一步上面请花费一定的时间。我建议你通过代码来实现约束,这样可以更好的明白你在哪里添加了这些约束,同时对于调试来说,这可以更好的找到错误。通过代码添加约束比通过interface Builder更加方便、有效,特别是通过某些别人已经设置好的API。这里是我所设计、维护、使用的一个第三方库(译者表示:我更喜欢用Masonry和SnapKit)

  • 如果你在代码中添加了约束,你将至少使用一次updateConstraints方法来设置你的UITableViewCell的subClass。注意:因为updateConstraints在你的代码中将不止一次的被调用,所以不要反复添加一样的约束,通过检查didSetupConstraints函数返回的布尔值(boolean)来确定updateConstraints中包含的内容是否只包含了一次。(你可以在你运行了一次你的约束后,将他设置为YES)。另一方面,如果你有代码来更新已经存在的约束(比如说调整某些约束的常量属性),请将这些更新的代码放在updateConstraints中,但是在检查didSetupConstraints以外。这样你可以在每次运行的时候都调用它。

#####2.设置TableViewCell的唯一标示符。
对于每一个唯一的cell中的约束,使用一个唯一的标示符来设置有这些约束的Cell。另一方面来说,如果你的cell有不止一个的独一无二的layout,每一个唯一的layout都应该有一个他自己的标示符。(这里有一个建议,一般你只有当你的Cell的subViews发生改变或者他们按照不同位置发生排列的时候,才会用新的标示符。)

比如说,如果你在每个cell中展示每一条email的内容的时候,你可能需要4种展示方式:有主题的邮件,有主题和文章主体的邮件,有主题和照片附件的邮件,有主题、文章主体还有照片附件的邮件。每种展示方式都有自己的不同约束来实现它,所以当你的cell创建并且添加约束的时候,每种不同的cell需要给予特定的标示符。这意味着当你将cell放入缓存池的时候,他们当中已经加入了各自的约束,并且已经设定好了他们的类型。

注意:由于内容的大小不同,所以cell有着一样的约束可能仍旧有不同的高度。不要因为内容有着不同的大小,却需要设置不同的layout(不同的约束)和计算框架方式不同(通过相同的约束进行解决)而感到迷惑。

  • 不要将有着完全不同约束的cell添加到同一个缓存池(reuse pool)(比如说使用相同的标示符),同时企图删掉老的约束来重新开始来设置每一个缓寸池中的cell。因为Autolayout的内部不是用来进行大规模的约束变化,如果你这么做的话,你将会看到明显的性能问题。

####对于iOS 8 - 自行调整大小的cell

3.使用行高的自动估计

对于iOS 8 ,苹果公司已经在你使用iOS 8之前内化了大量的工作,为了能够使用这种行高的自动估计,你必须首先设置他的rowHeight这一属性,一般我们把它设置为UITableViewAutomaticDimension。然后你需要设置tableView的estimatedRowHeight来开启行高估计,同时这个属性不能为0,下面是一个例子:

1
2
self.tableView.rowHeight = UITableViewAutomaticDimension;
self.tableView.estimatedRowHeight = 44.0; // 设置你cell的均值

这是为了在你的cell还没有显示到屏幕上之前,先给予你的tableView中的每一个cell一个临时的估计高度(占位符)。为了得到每一行的准确高度,tableView自动询问每一个cell,他们自己内部基于cell自身固定宽度的内容视图,从而确定自身的contentView的高度(这将基于tableView自己的宽度减去外部的索引或者他的附属视图)和你添加到cell的View和SubView的autolayout的约束。当cell的实际高度被计算出来之后,老的估算高度将会被更新为新的高度,同时它将调整为你所需要的tableView的contentSizecontentOffset

总的来说,你所设置的估算高度不会被真的调用,这只是用来设置tableView中正确大小的滚动指示符,同时在你的cell被显示到屏幕上的时候,这种方法将很好的调整那些不正确的估算值。你应该设置estimatedRowHeight这个tableView中的属性(一般在viewDidLoad或者和他类似的方法中)为行的均高。只有当你的行高产生极端变化(例如相差了一个数量级),你就会发现你的滚动指示符发生“跳动”的时候,你才需要费心去实现tableView:estimatedHeightForRowAtIndexPath方法,从而去做最小的计算,从而返回更加精确的cell的高度值。

####对于iOS 7 - 自行实现行高的自动调整

3.实现一个合理的布局从而获得cell的高度

首先实例化一个不会出现在屏幕上的tableViewCell的例子,这个cell将会被设置一个所有cell都会使用的重用表示符,这个cell将被进行严格的高度计算(不会出现在屏幕上意味着他只是一个属性,同时将不会在tableView:cellForRowAtIndexPath:调用,这意味他不会被渲染到屏幕上)然后,这个cell将会被添加将来显示在tableView中的具体的内容,(比如说文本,图片等等)。

然后迫使这个cell立即调整他的subView的布局,同时使用UItableViewCell的content的systemLayoutSizeFittingSize:方法来获得这个cell所需要的高度。使用UILayoutFittingCompressedSize来获得用来撑开cell的内容的最小的大小。高度必须靠代理中的tableView:heightForRowAtIndexPath:方法来返回。

4.使用预设置的行高

如果你的tableView有多行的内容,你将会autolayout的约束将会使得主线程在第一次加载tableView的时候产生问题,那是因为tableView:heightForRowAtIndexPath:方法将会在每一行第一次被调用的时候。(为了计算滚动指示符正确的大小)

对于iOS 7,你能够(或者说你绝对应该)在tableView中使用estimatedRowHeight属性,在没有被显示到屏幕上前,这将会给予tableView一个预估的行高(占位符)。然后这个cell在放到屏幕上的时候,每一行的高度将会被计算(通过调用tableView:heightForRowAtIndexPath:方法),同时这个真实高度将替代原有的预估的行高(占位符)。

//这一段和iOS 8的第三段一模一样
总的来说,你所设置的估算高度不会被真的调用,这只是用来设置tableView中正确大小的滚动指示符,同时在你的cell被显示到屏幕上的时候,这种方法将很好的调整那些不正确的估算值。你应该设置estimatedRowHeight这个tableView中的属性(一般在viewDidLoad或者和他类似的方法中)为行的均高。只有当你的行高产生极端变化(例如相差了一个数量级),你就会发现你的滚动指示符发生“跳动”的时候,你才需要费心去实现tableView:estimatedHeightForRowAtIndexPath方法,从而去做最小的计算,从而返回更加精确的cell的高度值。

5.添加行高缓存

如果你已经做了以上所有步骤,但是仍旧不能忍受设备因为调用tableView:heightForRowAtIndexPath:方法去计算约束时候显示tableView的缓慢,你可能很不幸的需要缓存cell的高度(这个建议来自于苹果的工程师)。总的想法是通过autolayout来计算第一次约束,然后通过缓存来计算将来需要使用的所有cell的高度。在使用这个方法的时候,你需要清楚的知道缓存中的行高和什么时候缓存中的行高将会发生改变。这将用于当cell的内容发生改变或者一些其他重要的事情发生的时候(比如说用户通过滑块来动态的调整文本的大小时候)

iOS 7简单的代码实现(基于很多建议)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
//设置标示符
static NSString *reuseIdentifier = ...;
//获取缓存池中的cell
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:reuseIdentifier];
// 设置cell的内容
// cell.textLabel.text = someTextForThisCell;
// ...
// 确定约束已经被添加
// 或者对约束进行调整
// 如果你确定约束已经被设定好,记得更新约束
// 更新的方法: [cell setNeedsUpdateConstraints];
[cell updateConstraintsIfNeeded];
// 如果你使用多行的Label,不要忘记正确的使用preferredMaxLayoutWidth
// 记得在子类中也进行调用
// -[layoutSubviews] 方法. For example:
// cell.multiLineLabel.preferredMaxLayoutWidth = CGRectGetWidth(tableView.bounds);
return cell;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
// Determine which reuse identifier should be used for the cell at this
// index path.
NSString *reuseIdentifier = ...;
// Use a dictionary of offscreen cells to get a cell for the reuse
// identifier, creating a cell and storing it in the dictionary if one
// hasn't already been added for the reuse identifier. WARNING: Don't
// call the table view's dequeueReusableCellWithIdentifier: method here
// because this will result in a memory leak as the cell is created but
// never returned from the tableView:cellForRowAtIndexPath: method!
UITableViewCell *cell = [self.offscreenCells objectForKey:reuseIdentifier];
if (!cell) {
cell = [[YourTableViewCellClass alloc] init];
[self.offscreenCells setObject:cell forKey:reuseIdentifier];
}
// Configure the cell with content for the given indexPath, for example:
// cell.textLabel.text = someTextForThisCell;
// ...
// Make sure the constraints have been set up for this cell, since it
// may have just been created from scratch. Use the following lines,
// assuming you are setting up constraints from within the cell's
// updateConstraints method:
[cell setNeedsUpdateConstraints];
[cell updateConstraintsIfNeeded];
// Set the width of the cell to match the width of the table view. This
// is important so that we'll get the correct cell height for different
// table view widths if the cell's height depends on its width (due to
// multi-line UILabels word wrapping, etc). We don't need to do this
// above in -[tableView:cellForRowAtIndexPath] because it happens
// automatically when the cell is used in the table view. Also note,
// the final width of the cell may not be the width of the table view in
// some cases, for example when a section index is displayed along
// the right side of the table view. You must account for the reduced
// cell width.
cell.bounds = CGRectMake(0.0f, 0.0f, CGRectGetWidth(tableView.bounds), CGRectGetHeight(cell.bounds));
// Do the layout pass on the cell, which will calculate the frames for
// all the views based on the constraints. (Note that you must set the
// preferredMaxLayoutWidth on multi-line UILabels inside the
// -[layoutSubviews] method of the UITableViewCell subclass, or do it
// manually at this point before the below 2 lines!)
[cell setNeedsLayout];
[cell layoutIfNeeded];
// Get the actual height required for the cell's contentView
CGFloat height = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
// Add an extra point to the height to account for the cell separator,
// which is added between the bottom of the cell's contentView and the
// bottom of the table view cell.
height += 1.0f;
return height;
}
// NOTE: Set the table view's estimatedRowHeight property instead of
// implementing the below method, UNLESS you have extreme variability in
// your row heights and you notice the scroll indicator "jumping"
// as you scroll.
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath
{
// Do the minimal calculations required to be able to return an
// estimated row height that's within an order of magnitude of the
// actual height. For example:
if ([self isTallCellAtIndexPath:indexPath]) {
return 350.0f;
} else {
return 40.0f;
}
}

####简单的Demo工程

这几个工程完全解决了因为UILabel的情况下tableView需要中cell的高度需要动态调整的问题。

你可以随时提出任何问题或者你所遇到的问题(你可以在Github上提交你的评论),我将尽力帮助你!

####Xamarin(C# / .NET)
如果你使用Xamarin,可以检出KentBoogaart的一个简单的工程